Skip to content

Implement IRenderer3D on the OpenGL backend - #599

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/project-thread-lrlxwz
Oct 8, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/project-thread-lrlxwz

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Requested by Matt · project thread

Before: ImGuiApp.TryGetRenderer3D answered false in a real desktop app, because only the software renderer implemented IRenderer3D. The Viewport3D demo fell back to a CPU wireframe outside the test harness.

After: the OpenGL backend implements it. TryGetRenderer3D answers true on desktop, and a caller can draw depth-tested, culled, textured geometry into offscreen targets and show them with ImGui.Image. The same matrix draws the same upright picture on OpenGL and on the software renderer.

How

  • GLRenderer3D (new, internal) does the work behind an internal IGL3D seam. GL3DWrapper carries it to Silk.NET, and ImGuiController delegates its new IRenderer3D members to it. IGL3D is separate from the public IGL so that no existing implementer breaks.
  • A target is an FBO with an RGBA8 colour texture and, when depth: true, a D24 renderbuffer. The handle is the colour texture's name. That is the only value that cannot collide with a live CreateTexture name, so both crossover guards hold: DeleteTexture throws on a target, and DeleteRenderTarget throws on anything else. Neither one frees anything when it refuses.
  • Clip-space convention: depth runs [0, 1], which is what Matrix4x4.CreatePerspectiveFieldOfView produces and what the software renderer's DepthBuffer uses. The vertex shader remaps z onto OpenGL's [-w, w]. It also negates y so that row 0 is the top, which is how ImGui.Image shows it. Because that flips winding, front faces are GL_CW while drawing.
  • Clear and Draw restore everything they touch: both draw and read framebuffer bindings (restored to their previous values, not to 0), program, active texture with its unit-0 texture and sampler, VAO, array buffer, depth test/mask/func, cull enable/mode/front face, blend enable/equations/funcs, scissor, viewport and clear values.
  • An empty index span is a no-op, so nothing gets compiled or bound. An out-of-range index throws ArgumentOutOfRangeException before anything reaches the driver. ResizeRenderTarget respecifies storage in place and keeps the handle. The program and buffers are created lazily on the first draw.

Tests

GLRenderer3DTests has 26 tests that run against a stateful stand-in for the context. They cover allocation and release, an incomplete framebuffer, both delete guards (including through ImGuiController.DeleteTexture), resize, line lists, matrix upload order, the Vertex3D byte layout, and state restore. The restore tests start from a context left in deliberately unusual state and compare a full snapshot before and after. To check the tests can fail, I removed the depth-test restore, the y flip and the index check one at a time, and each change failed a test. The full ImGui.App.Tests suite passes (523 passed, 2 skipped), and the solution builds clean in Release with the ktsu analyzers on.

I also checked it on a real context (Mesa llvmpipe, GL 4.5, under Xvfb) with a scratch program that is not committed. It cleared a 64×64 target, drew overlapping quads with depth, a back-wound quad and a perspective triangle, then read the texture back. The output was upright, the depth test worked, the back-wound quad was culled, the perspective triangle passed clipping, there was no GL error, and the depth test and culling were off again afterwards.

CLAUDE.md's Backend-agnostic 3D section now describes the OpenGL implementation. Metal (#413 sub-issue 3) and Viewport3D (sub-issue 4) are still open.

Fixes #414

🤖 Generated with Claude Code

https://claude.ai/code/session_012BGwLwAUhH4nnCmtfec9ob


Generated by Claude Code

ImGuiController now implements IRenderer3D through GLRenderer3D: a render
target is a framebuffer with an RGBA8 colour texture and an optional D24
renderbuffer, drawn with a second shader program created on first use.
TryGetRenderer3D therefore answers true in a desktop application.

- A target's handle is its colour texture's name, so it can never collide
  with a CreateTexture name. DeleteTexture refuses a target and
  DeleteRenderTarget refuses anything else, freeing nothing either way.
- The shader maps clip z from [0, w] onto OpenGL's [-w, w] and flips y, so
  one Matrix4x4.CreatePerspectiveFieldOfView matrix draws the same upright
  picture as the software renderer; front faces are GL_CW while drawing.
- Clear and Draw restore every piece of state they touch, including both
  framebuffer bindings, depth test/mask/func, culling and winding, the
  viewport, scissor and clear values.
- Empty index spans are a no-op; an out-of-range index throws
  ArgumentOutOfRangeException before anything reaches the driver.

The calls go through an internal IGL3D seam, and GLRenderer3DTests covers
allocation, release, both delete guards, resize, and state restore against
a stateful stand-in.

Fixes #414

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BGwLwAUhH4nnCmtfec9ob
@matt-edmondson matt-edmondson self-assigned this Oct 7, 2026
…om coverage

GL3DWrapper only forwards to a live OpenGL context, which unit tests do not
have, so it follows the ExcludeFromCodeCoverage precedent for native interop.
The controller's IRenderer3D members and the uninitialised guard are now
exercised, as is a second Dispose.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BGwLwAUhH4nnCmtfec9ob
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IRenderer3D, offscreen render targets, and the OpenGL implementation

2 participants